// CyTargetLinker,
// a Cytoscape plugin to extend biological networks with regulatory interaction
// Copyright 2011-2012 BiGCaT Bioinformatics
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package cytargetlinker;

import giny.model.Node;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.swing.SwingConstants;

import cytargetlinker.dialogs.CyTargetLinkerDialog;
import cytoscape.CyEdge;
import cytoscape.CyNetwork;
import cytoscape.CyNode;
import cytoscape.Cytoscape;
import cytoscape.data.CyAttributes;
import cytoscape.layout.CyLayoutAlgorithm;
import cytoscape.layout.CyLayouts;
import cytoscape.task.ui.JTaskConfig;
import cytoscape.task.util.TaskManager;
import cytoscape.view.CyNetworkView;
import cytoscape.view.cytopanels.CytoPanelState;
import cytoscape.visual.CalculatorCatalog;
import cytoscape.visual.VisualMappingManager;
import cytoscape.visual.VisualStyle;

/**
 * main class of the plugin that collects the data 
 * and distributes the different tasks to the right classes
 */
public class CyTargetLinker implements PropertyChangeListener {

	private CyTargetLinkerPlugin plugin;
	
	// network provided by the user
	private CyNetwork userNetwork;
	// identifierAttribute, node-identifier
	private Map<String, CyNode> userNetworkIds;
	// node attribute that contains the identifier in the user network
	private String identifierAttribute;
	
	// list of regulatory interaction networks
	private List<RegulatoryInteractionNetwork> regulatoryInteractionNetworks;
	private String statusMessage = "";
	
	// resulting network 
	// TODO: allow the user to decide if a new network should be created or not
	private CyNetwork resultingNetwork;
	// name of the new network
	private String resultingNetworkName;
	// nodes that will be added to the network
	private Map<String, CyNode> newNodes;
	// visual style of the new network
	// TODO: different color of nodes for different edge types (e.g. miRNA and transcription factors)
	private VisualStyle visualStyle;
	
	// tab that contains the result information
	private ResultTab tab;
	// decides if the nodes in the user network should only be targets or sources or both
	private Direction direction = Direction.BOTH;

	public enum Direction {
		SOURCE,
		TARGET,
		BOTH
	}
	
	public CyTargetLinker(CyTargetLinkerPlugin plugin) {
		this.plugin = plugin;
		Cytoscape.getPropertyChangeSupport().addPropertyChangeListener(Cytoscape.NETWORK_DESTROYED, this);
		userNetworkIds = new HashMap<String, CyNode>();
		regulatoryInteractionNetworks = new ArrayList<RegulatoryInteractionNetwork>();
		newNodes = new HashMap<String, CyNode>();
//		removedNodes = new ArrayList<Node>();
	}
	
	/**
	 * opens the load interactions dialog
	 */
	public void openDialog() {
		userNetworkIds.clear();
		regulatoryInteractionNetworks.clear();
		CyTargetLinkerDialog dialog = new CyTargetLinkerDialog(this);
		dialog.setVisible(true);
	}
	
	/**
	 * starts the merge task (add interactions from the 
	 * RINs into the resulting network)
	 * @param rins
	 */
	public void startMerge(List<File> files) {
		MergeTask task = new MergeTask(this, files);
		JTaskConfig jTaskConfig = new JTaskConfig();
		jTaskConfig.setOwner(Cytoscape.getDesktop());
		jTaskConfig.displayCloseButton(true);
		jTaskConfig.displayCancelButton(true);
		jTaskConfig.displayStatus(true);
		jTaskConfig.setAutoDispose(false);
		
		// Execute Task in New Thread; pops open JTask Dialog Box.
		TaskManager.executeTask(task, jTaskConfig);
		
		Cytoscape.getDesktop().getCytoPanel(SwingConstants.EAST).setState(CytoPanelState.DOCK);
	}

	public void setUserNetwork(CyNetwork network, String idAttribute) {
		if(network != null && idAttribute != null && !idAttribute.equals("")) {
			userNetwork = network;
			identifierAttribute = idAttribute;
		}
	}
	
	
	
	@SuppressWarnings("unchecked")
	public void findUserNetworkIdentifiers() {
		CyAttributes cyNodeAttrs = Cytoscape.getNodeAttributes();
		getUserNetworkIds(userNetwork.nodesList(), cyNodeAttrs);
	}
	
	/**
	 * adds regulatory interactions to the resulting nMartina KutmonHomeetwork
	 */
	@SuppressWarnings("unchecked")
	public void mergeNetworks() {		
		checkNetName();
		// copy user network into new network
		resultingNetwork = Cytoscape.createNetwork(userNetwork.nodesList(), userNetwork.edgesList(), resultingNetworkName);
		
		// get attribute(s) for the User node(s).....
		CyAttributes cyNodeAttrs = Cytoscape.getNodeAttributes();
		CyAttributes cyEdgeAttrs = Cytoscape.getEdgeAttributes();
		
		// add a new attribute for the network nodes in the new network
		addAttributeToPanel(resultingNetwork.nodesList(), cyNodeAttrs);

		// adds new nodes and edges to the resulting network
		addNewEdgesToNetwork(cyNodeAttrs, cyEdgeAttrs);

		// apply treshhold = 1
		RINHandler.getHandler().applyThreshold(this);
		
		// apply force directed layout
		applyLayout();
		
		// creates CyTargetLinker visual style
		createVisualStyle();
	}
	
	public void applyLayout() {
		int count = 0;
		for(RegulatoryInteractionNetwork net : regulatoryInteractionNetworks) {
			count = count + net.getNewInteractions();
		}
		
		count = count - RINHandler.getHandler().getRemovedNodes().size();
		if(count < 10000) {
			CyLayoutAlgorithm alg = CyLayouts.getLayout("force-directed");
			alg.doLayout();
		}
	}
	
	public void applyVisualStyle() {
		CyTargetLinkerVisualStyle style = new CyTargetLinkerVisualStyle();	
		Cytoscape.getNetworkView(resultingNetwork.getIdentifier()).applyVizmapper(style.createVisualStyle(this));
		Cytoscape.createNetworkView(resultingNetwork, "CyTargetLinkerView");
	}

	private int treshhold = 1;
	
	public int getThreshold() {
		return treshhold;
	}

	public void setTreshhold(int treshhold) {
		this.treshhold = treshhold;
	}



	public Map<String, CyNode> getNewNodes() {
		return newNodes;
	}

	public void createVisualStyle() {
		
		CyNetworkView view = Cytoscape.getNetworkView(resultingNetworkName);
		
		// get the VisualMappingManager and CalculatorCatalog
		VisualMappingManager manager = Cytoscape.getVisualMappingManager();
		CalculatorCatalog catalog = manager.getCalculatorCatalog();

		// check to see if a visual style with this name already exists
		visualStyle = catalog.getVisualStyle(Preferences.getInstance().getVsName());
		if (visualStyle == null) {
			// if not, create it and add it to the catalog
			CyTargetLinkerVisualStyle style = new CyTargetLinkerVisualStyle();

			visualStyle = style.createVisualStyle(this);
			catalog.addVisualStyle(visualStyle);
		}

		view.setVisualStyle(visualStyle.getName()); // not strictly necessary

		// actually apply the visual style
		manager.setVisualStyle(visualStyle);
	}
	
	/**
	 * adds all new edges and nodes for all RINs in the resulting network
	 */
	private void addNewEdgesToNetwork(CyAttributes cyNodeAttrs, CyAttributes cyEdgeAttrs) {
		for(RegulatoryInteractionNetwork network : regulatoryInteractionNetworks) {
			for(cytargetlinker.io.Edge edge : network.getEdges()) {
				cytargetlinker.io.Node source = edge.getSource();
				cytargetlinker.io.Node target = edge.getTarget();
			
				CyNode cySource = null;
				CyNode cyTarget = null;
				if(direction == Direction.BOTH || direction == Direction.SOURCE) {
					cySource = findMatchingCyNode(source);
					cyTarget = createNewNode(target, cyNodeAttrs, network);
					createEgde(cySource, cyTarget, edge, network, cyEdgeAttrs);
				}
				if(direction == Direction.BOTH || direction == Direction.TARGET) {
					cySource = createNewNode(source, cyNodeAttrs, network);
					cyTarget = findMatchingCyNode(target);
					createEgde(cySource, cyTarget, edge, network, cyEdgeAttrs);
				} 
			}
		}
	}
	
	/**
	 * creates a new edge in the resulting network
	 */
	private void createEgde(CyNode source, CyNode target, cytargetlinker.io.Edge edge, RegulatoryInteractionNetwork network, CyAttributes cyEdgeAttrs) {
		if(source != null && target != null) {
			CyEdge cyEdge = createNewEdge(source.getIdentifier(), target.getIdentifier(), edge, network.getName(), network.getEdgeType(), cyEdgeAttrs);
			edge.setCySource(source);
			edge.setCyTarget(target);
			network.getCyEdges().add(cyEdge);
			resultingNetwork.addEdge(cyEdge);
		}
	}
	
	/**
	 * checks if the user network does not contain a node
	 * with one of the identifiers specified
	 */
	private CyNode findMatchingCyNode(cytargetlinker.io.Node node) {
		for(String id : node.getIdentifiers()) {
			if(userNetworkIds.containsKey(id)) {
				return userNetworkIds.get(id);
			}
		}
		return null;
	}
	
	/**
	 * creates a new node if the node is not present in the user network
	 */
	private CyNode createNewNode(cytargetlinker.io.Node node, CyAttributes cyNodeAttrs, RegulatoryInteractionNetwork network) {
		CyNode cyNode = null;

		String id = node.getId();
		for(String str : node.getIdentifiers()) {
			if(userNetworkIds.containsKey(str)) {
				id = str;
				break;
			}
		}

		if(!userNetworkIds.containsKey(id)) {
			for(String str : node.getIdentifiers()) {
				if(newNodes.containsKey(str)) {
					network.getCyNodes().add(newNodes.get(str));
					return newNodes.get(str);
				}
			}
			
			cyNode = Cytoscape.getCyNode(id, true);
			for(String str : node.getProperties().keySet()) {
				cyNodeAttrs.setAttribute(cyNode.getIdentifier(), str, node.getProperties().get(str));
			}
			if(node.getIdentifiers().size() > 1) {
				cyNodeAttrs.setListAttribute(cyNode.getIdentifier(), "identifiers", node.getIdentifiers());
			}
			cyNodeAttrs.setAttribute(cyNode.getIdentifier(), "label", getLabel(cyNode, cyNodeAttrs));
			cyNodeAttrs.setAttribute(cyNode.getIdentifier(), Preferences.getInstance().getNodeTypeAttribute(), Preferences.getInstance().getNewNodeValue());
			resultingNetwork.addNode(cyNode);
			network.getCyNodes().add(cyNode);
			
			for(String str : node.getIdentifiers()) {
				newNodes.put(str, cyNode);
			}
		} else {
			cyNode = userNetworkIds.get(id);
		}
		
		return cyNode;
	}
	
	/**
	 * creates a new edge with a specific interaction type of the regulatory
	 * interaction network
	 */
	private CyEdge createNewEdge(String sourceId, String targetId, cytargetlinker.io.Edge edge, String networkName, String edgeType, CyAttributes cyEdgeAttrs) {
		CyEdge cyEdge = Cytoscape.getCyEdge(sourceId, edgeType, targetId, networkName);
		
		for(String str : edge.getProperties().keySet()) {
			cyEdgeAttrs.setAttribute(cyEdge.getIdentifier(), str, edge.getProperties().get(str));
		}
		cyEdgeAttrs.setAttribute(cyEdge.getIdentifier(), "interaction", networkName);
		return cyEdge;
	}
	
	/**
	 * checks if network name is already given
	 */
	private void checkNetName() {
		Set<CyNetwork> networks = Cytoscape.getNetworkSet();
		for(CyNetwork net : networks) {
			if(net.getTitle().equals(resultingNetworkName)) {
				setResultingNetworkName(resultingNetworkName + "_1");
				checkNetName();
			}
		}
	}

	/**
	 * saves all node identifiers from the user network in the
	 * userIdMultiMap
	 */
	@SuppressWarnings("unchecked")
	private void getUserNetworkIds(List<CyNode> nodes, CyAttributes cyNodeAttrs) {
		
		for (CyNode node : nodes) {
			Set<String> values = new HashSet<String>();
			
			byte type = cyNodeAttrs.getType(identifierAttribute);
			switch(type) {
			case CyAttributes.TYPE_STRING:
				values.add(cyNodeAttrs.getStringAttribute(node.getIdentifier(), identifierAttribute));
				break;
			case CyAttributes.TYPE_SIMPLE_LIST:
				values.addAll(cyNodeAttrs.getListAttribute(node.getIdentifier(), identifierAttribute));
				break;
			default:
				//warning
			}
			
			for(String id : values) {
				if(id != null && !"".equals(id)) {
					userNetworkIds.put(id, node);
				}
			}
		}
	}

	/**
	 * adds the node type attribute
	 * network node = node that existed in the user network
	 * new node = node that was added because it has a regulatory interaction in a RIN
	 */
	private void addAttributeToPanel(List<Node> nodesList, CyAttributes cyNodeAttrs) {
		// adding the new attribute to the User node(s).....
		for (Node node : nodesList) {
			
			// check if node has value in attribute NodeType
			String value = cyNodeAttrs.getStringAttribute(node.getIdentifier(),  Preferences.getInstance().getNodeTypeAttribute());
			
			// if not = set value to NetworkNode
			if(value == null || value.equals("")) {
				if(isNetworkNode(node, cyNodeAttrs)) {
					cyNodeAttrs.setAttribute(node.getIdentifier(),  Preferences.getInstance().getNodeTypeAttribute(), Preferences.getInstance().getNetworkNodeValue());
					cyNodeAttrs.setAttribute(node.getIdentifier(),  "label", getLabel(node, cyNodeAttrs));
				} else {
					cyNodeAttrs.setAttribute(node.getIdentifier(),  Preferences.getInstance().getNodeTypeAttribute(), Preferences.getInstance().getGroupNodeValue());
				}
			} else {
				
				// if value is not NewNode - set value to NetworkNode
				if(!value.equals(Preferences.getInstance().getNewNodeValue())) {
					if(isNetworkNode(node, cyNodeAttrs)) {
						cyNodeAttrs.setAttribute(node.getIdentifier(),  Preferences.getInstance().getNodeTypeAttribute(), Preferences.getInstance().getNetworkNodeValue());
						cyNodeAttrs.setAttribute(node.getIdentifier(),  "label", getLabel(node, cyNodeAttrs));
					} else {
						cyNodeAttrs.setAttribute(node.getIdentifier(),  Preferences.getInstance().getNodeTypeAttribute(), Preferences.getInstance().getGroupNodeValue());
					}
				}
			}
		}

		// Inform others via property change event.
		Cytoscape.firePropertyChange(Cytoscape.ATTRIBUTES_CHANGED, null, null);
	}
	
	/**
	 * if a node has an attribute "label" or "Label"
	 * it is used to label the node
	 * otherwise the user would see the often unreadable identifiers
	 */
	private String getLabel(Node node, CyAttributes cyNodeAttrs) {
		String label = cyNodeAttrs.getStringAttribute(node.getIdentifier(), "label");
		if(label == null) {
			label = cyNodeAttrs.getStringAttribute(node.getIdentifier(), "Label");
			if(label == null) {
				String name = cyNodeAttrs.getStringAttribute(node.getIdentifier(), "canonicalName");
				if(name != null && !name.equals("")) {
					label = name;
				} else {
					label = node.getIdentifier();
				}
			}
		}
		return label;
	}
	
	/**
	 * checks if a node exists in the user network
	 */
	private boolean isNetworkNode(Node node, CyAttributes cyNodeAttrs) {
		String nodeTypeAttribute = cyNodeAttrs.getStringAttribute(node.getIdentifier(),  Preferences.getInstance().getNodeTypeAttribute());
		
		if(nodeTypeAttribute == null) {
			if(!isGroupNode(node, cyNodeAttrs)) {
				return true;
			}
		} else {
			// if value is not NewNode - set value to NetworkNode
			if(!nodeTypeAttribute.equals(Preferences.getInstance().getNewNodeValue())) {
				if(!isGroupNode(node, cyNodeAttrs)) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * checks if a node is a GPML group node
	 */
	private boolean isGroupNode(Node node, CyAttributes cyNodeAttrs) {
		String groupStyle = cyNodeAttrs.getStringAttribute(node.getIdentifier(), "GroupStyle");
		if(groupStyle != null && (groupStyle.equals("Group") || groupStyle.equals("Complex"))) {
			return true;
		}
		String anchorStyle = cyNodeAttrs.getStringAttribute(node.getIdentifier(), "StartLineType");
		if(anchorStyle != null) {
			return true;
		}
		return false;
	}

	/**
	 * adds the tab to the result panel
	 */
	public void createColorTab() {
		tab = new ResultTab(this);
		tab.fillTab();
	}

	/**
	 * removes the tab in the result panel 
	 * if the network is destroyed
	 */
	@Override
	public void propertyChange(PropertyChangeEvent event) {
		if (event.getPropertyName().equalsIgnoreCase(Cytoscape.NETWORK_DESTROYED)) {
			if(resultingNetwork != null) {
				String id = resultingNetwork.getIdentifier();
				if(event.getNewValue().equals(id)) {
					tab.removeTab();
				}
			}
		}
	}
	
	/** SETTERS & GETTERS **/
	
	public void setDirection(Direction direction) {
		this.direction = direction;
	}

	public String getResultingNetworkName() {
		return resultingNetworkName;
	}

	public void setResultingNetworkName(String resultingNetworkName) {
		this.resultingNetworkName = resultingNetworkName;
	}

	public CyTargetLinkerPlugin getPlugin() {
		return plugin;
	}

	public List<RegulatoryInteractionNetwork> getRegulatoryInteractionNetworks() {
		return regulatoryInteractionNetworks;
	}

	public CyNetwork getResultingNetwork() {
		return resultingNetwork;
	}

	public String getStatusMessage() {
		return statusMessage;
	}

	public void setStatusMessage(String statusMessage) {
		this.statusMessage = statusMessage;
	}

	public Map<String, CyNode> getUserNetworkIds() {
		return userNetworkIds;
	}
}
